
package jfireeagle;

import java.net.URL;
import java.util.*;

import org.apache.http.client.*;
import org.apache.http.client.params.AllClientPNames;
import org.apache.http.impl.client.DefaultHttpClient;

import com.thoughtworks.xstream.XStream;

import net.oauth.OAuthAccessor;
import net.oauth.OAuthConsumer;
import net.oauth.OAuthMessage;
import net.oauth.OAuthServiceProvider;
import net.oauth.client.OAuthClient;
import net.oauth.client.httpclient4.HttpClient4;
import net.oauth.client.httpclient4.HttpClientPool;
import net.oauth.http.HttpMessageDecoder;

import jfireeagle.xml.*;

/**
 * 
 * 
 *   Fire Eagle client
 * 
 *   @author Sean C. Sullivan
 *   
 */
public class FireEagleClient
{
	private XStream xstream;
	private ClientSettings settings;
	private HttpClient httpClient;
	private OAuthClient authClient;
	private OAuthServiceProvider authProvider;
	private Token requestToken = new Token();
	private boolean compressionEnabled = false;
	private HttpClientPool pool;
	
	public FireEagleClient()
	{
		this(new DefaultHttpClient(), null);
	}
	
	public FireEagleClient(ClientSettings cs)
	{
		this(new DefaultHttpClient(), cs);
		
	}
	
	public FireEagleClient(final HttpClient hClient)
	{
		this(hClient, null);
	}
	
	public FireEagleClient(final HttpClient hClient, ClientSettings cs)
	{
		this.httpClient = hClient;
		
		this.setClientSettings(cs);
		
		setUserAgent("jfireeagle");
		setConnectionTimeout(15 * 1000);
		setSocketTimeout(30 * 1000);
		
		
		this.pool = new HttpClientPool()
		{

			public HttpClient getHttpClient(URL u)
			{
				return httpClient;
			}
			
		};
		
	}

	public void updateLocation(String freeText)
	{
		LocationParameters loc = new LocationParameters();
		
		loc.setFreeText(freeText);
		
		updateLocation(loc);
		
	}
	
	public void updateLocation(double latitude, double longitude)
	{
		GpsCoordinate coordinate = new GpsCoordinate(latitude, longitude);
		
		LocationParameters loc = new LocationParameters();
		
		loc.setGpsCoordinate(coordinate);
		
		updateLocation(loc);
	}
	
	public void updateLocation(LocationParameters loc)
	{
		updateLocation(Util.toMap(loc));
	}
	
	
	
	protected boolean updateLocation(Map<String, String> params)
	{
		Response r = sendFireEagleRequest("POST", "update.xml", params, this.getUserSpecificAccessToken());
		
		// todo : return a result???
		
		return true;
	}

	public Locations lookup(String freeText)
	{
		LocationParameters loc = new LocationParameters();
		
		loc.setFreeText(freeText);
		
		return lookup(loc);
		
	}

	public Locations lookup(LocationParameters params)
	{
		return lookup(Util.toMap(params));
	}
	

	protected Locations lookup(Map<String, String> params)
	{
		Response r = sendFireEagleRequest("GET", "lookup.xml", params, this.getGeneralAccessToken());
		
		return r.getLocations();
	}
	
	
	protected Token getUserSpecificAccessToken()
	{
		return this.getClientSettings().getUserSpecificToken();
	}

	/**
	 * 
	 * 
	 * Returns a list of users of the application who have updated 
	 * their location within the past 12 hours
	 * 
	 */
	public List<User> getRecentTwelveHours()
	{
		Calendar c = Calendar.getInstance();
		
		c.add(Calendar.HOUR_OF_DAY, -12);
		
		return getRecent(c);
	}

	/**
	 * 
	 * Returns a list of users of the application who have updated 
	 * their location within the past 24 hours
	 * 
	 */
	public List<User> getRecentTwentyFourHours()
	{
		Calendar c = Calendar.getInstance();
		
		c.add(Calendar.HOUR_OF_DAY, -24);
		
		return getRecent(c);
	}
	
	/**
	 * 
	 * 
	 * "Returns a list of users of the application who have updated 
	 *  their location within the given amount of time"
	 * 
	 */
	public List<User> getRecent(java.util.Calendar since)
	{
		return getRecent(since.getTimeInMillis());
	}

	/**
	 * 
	 * 
	 * "Returns a list of users of the application who have updated 
	 *  their location within the given amount of time"
	 * 
	 */
	public List<User> getRecent(java.util.Date since)
	{
		return getRecent(since.getTime());
	}
	
	/**
	 * 
	 * 
	 * "Returns a list of users of the application who have updated 
	 *  their location within the given amount of time"
	 * 
	 */
	public List<User> getRecent(long sinceTime)
	{
		Map<String, String> params = new LinkedHashMap<String, String>();
		
		params.put("time", "" + sinceTime);
		
		// Util.printTime(sinceTime);
		
		// todo:   params.put("per_page", x);
		// todo:   params.put("page", p);
		
		return getRecent(params);
	}
	
	protected List<User> getRecent(Map<String, String> params)
	{
		Response r = sendFireEagleRequest("GET", "recent.xml", params, this.getGeneralAccessToken());
		
		return r.getUsers(); 
		
	}
	
	
	public List<User> within(String freeText)
	{
		LocationParameters loc = new LocationParameters();
		loc.setFreeText(freeText);
		
		return within(Util.toMap(loc));
	}


	public List<User> within(LocationParameters loc)
	{
		return within(Util.toMap(loc));
	}

	
	/**
	 * 
	 *   Returns the location of a specific user in a location hierarchy format.
	 * 
	 */
	public User getUserLocation()
	{
		Map<String, String> params = new LinkedHashMap<String, String>();
		
		Response r = sendFireEagleRequest("GET", "user.xml", params, this.getUserSpecificAccessToken());
		
		return r.getUser();
	}
	
	protected Token getGeneralAccessToken()
	{
		return this.getClientSettings().getGeneralToken();
	}
	
	protected List<User> within(Map<String, String> params)
	{
		
		Response r = sendFireEagleRequest("GET", "within.xml", params, this.getGeneralAccessToken());
		
		return r.getUsers();
		
		
	}
	
	
	
	public ClientSettings getClientSettings()
	{
		if (this.settings == null)
		{
			this.settings = new ClientSettings();
		}
		return this.settings;
	}

	protected OAuthServiceProvider createOAuthServiceProvider()
	{
		
		OAuthServiceProvider p = new OAuthServiceProvider(
				this.getClientSettings().getOAuthRequestTokenUrl(),
				this.getClientSettings().getOAuthAuthorizeUrl(),
				this.getClientSettings().getOAuthAccessTokenUrl());
		
		return p;
		
	}
	
	protected OAuthServiceProvider getOAuthServiceProvider()
	{
		if (authProvider == null)
		{
			authProvider = createOAuthServiceProvider();
		}
		return authProvider;
	}
	
	protected OAuthAccessor createOAuthAccessor()
	{
		
		Token consumerToken = this.getConsumerToken();
		
		String oauthCallBackUrl = this.getClientSettings().getOAuthCallbackUrl();
		
		OAuthConsumer c = new OAuthConsumer(
									oauthCallBackUrl, 
									consumerToken.getPublicKey(), 
									consumerToken.getSecret(), 
									getOAuthServiceProvider());
		
		if (this.compressionEnabled)
		{
			c.setProperty(OAuthClient.ACCEPT_ENCODING, HttpMessageDecoder.ACCEPTED);
		}
		
		OAuthAccessor a = new OAuthAccessor(c);
		
		return a;
		
	}
	
	
	protected OAuthClient createOAuthClient()
	{
		
		OAuthClient authClient = new OAuthClient(new HttpClient4(this.pool));
		
		return authClient;
	}

	protected OAuthClient getOAuthClient()
	{
		
		if (authClient == null)
		{
			authClient = createOAuthClient();
		}
		
		return authClient;
	}

	
	protected Response sendFireEagleRequest(String method, String service, java.util.Map<String, String> params, Token token)
	{
		checkUserAccessToken();
		
		String xml = sendHttpRequest(this.getClientSettings().getWebServiceUrl() + "/" + service, method, params, token);
		
		Response r = fromXml(xml);
		
		return r;
	}
	
	protected void checkUserAccessToken()
	{
		if ( hasValidUserAccessToken() == false)
		{
			
			if (this.requestToken.isValid())
			{
				fetchAccessToken();
			}
			else
			{
				String userAuthorizationUrl = getUserAuthorizationUrl();
				
				throw new UserAuthorizationRequiredException(userAuthorizationUrl);
			}
		}
	}

	protected Token getConsumerToken()
	{
		return this.getClientSettings().getConsumerToken();
	}
	
	@SuppressWarnings("unchecked")
	public void fetchAccessToken()
	{
		
		// System.out.println("fetchAccessToken: requestToken=" + this.getRequestToken());
		
		OAuthClient client = getOAuthClient();
		
		OAuthMessage responseMsg = null;
		
		OAuthAccessor access = this.createOAuthAccessor();
		
		try
		{
			
				//
				//  request the Access token
				//
				
				access.accessToken = requestToken.getPublicKey();
				access.requestToken = requestToken.getPublicKey();
				access.tokenSecret = requestToken.getSecret();
				
				responseMsg = client.invoke(access, "GET", this.getClientSettings().getOAuthAccessTokenUrl(), null);
				
				this.getUserSpecificAccessToken().setPublicKey(responseMsg.getParameter("oauth_token"));
				this.getUserSpecificAccessToken().setSecret(responseMsg.getParameter("oauth_token_secret"));
			
		} catch (Exception e) {
			throw new FireEagleException(e);
		}
	}
	
	public String getUserAuthorizationUrl()
	{
		
		String url = null;

		OAuthClient client = getOAuthClient();
		
		OAuthAccessor access = this.createOAuthAccessor();
		
		try
		{
			this.requestToken = null;
			access.requestToken = null;
			access.accessToken = null;
			access.tokenSecret = null;
			
			//
			//  send GET request to the Request Token URL
			//
			
			client.getRequestToken(access);
			
			// store the Request Token values
			requestToken = new Token();
			requestToken.setPublicKey(access.requestToken);
			requestToken.setSecret(access.tokenSecret);
			
			
			//
			//
			// build user authorization URL
			//
			//
			
			url = this.getClientSettings().getOAuthAuthorizeUrl() 
						+ "?oauth_token=" + requestToken.getPublicKey();
			
			if (settings.getOAuthCallbackUrl() != null)
			{
					url = url + "&oauth_callback="
								+ settings.getOAuthCallbackUrl();
			}
			
			
			return url;
			
		} 
		catch (Exception e) 
		{
			throw new FireEagleException(e);
		}
		
		
	}

	
	protected String sendHttpRequest(String baseUrl, String method, java.util.Map<String, String> params, Token token)
	{
		
		OAuthClient client = getOAuthClient();
		
		OAuthMessage responseMsg = null;
		
		
		OAuthAccessor access = this.createOAuthAccessor();
		
		access.accessToken = token.getPublicKey();
		access.tokenSecret = token.getSecret();
		
		if (params == null)
		{
			params = new HashMap<String, String>();
		}
		
		
		try
		{
									
			responseMsg = client.invoke(access, method, baseUrl, params.entrySet());
			
			return responseMsg.readBodyAsString();
			
		} 
		catch (Exception e) 
		{
			throw new FireEagleException(e);
		}
		
	}
	
	
	
	protected boolean isEmpty(String s)
	{
		if (s == null)
		{
			return true;
		}
		else if (s.length() == 0)
		{
			return true;
		}
		else
		{
			return false;
		}
	}

	public void setClientSettings(ClientSettings s)
	{
		this.settings = s;
	}

	protected boolean hasValidUserAccessToken()
	{
		return this.getUserSpecificAccessToken().isValid();
	}
	
	protected boolean generalAccessTokenIsValid()
	{
		return this.getGeneralAccessToken().isValid();
	}
	
	protected Response fromXml(String xmlResponse)
	{
		Response r = (Response) this.getXStream().fromXML(xmlResponse);
		
		r.setXml(xmlResponse);
		
		return r;
	}
	
	protected XStream getXStream()
	{
		
		if (xstream == null)
		{
			return XStreamFactory.createXStream();
		}
		
		return xstream;
	}

	public Token getToken()
	{
		return this.requestToken;
	}

	public void shutdown()
	{
		try
		{
			this.getHttpClient().getConnectionManager().shutdown();
		}
		catch (Exception ignore)
		{
			// ignored
		}
		
	}
	

	public void setUserAgent(String ua)
	{
		getHttpClient().getParams().setParameter(AllClientPNames.USER_AGENT, ua);
	}
	
	public void setConnectionTimeout(int milliseconds)
	{
		getHttpClient().getParams().setIntParameter(AllClientPNames.CONNECTION_TIMEOUT, milliseconds);
	}
	
	public void setSocketTimeout(int milliseconds)
	{
		getHttpClient().getParams().setIntParameter(AllClientPNames.SO_TIMEOUT, milliseconds);
	}

	public void setCompressionEnabled(boolean b)
	{
		this.compressionEnabled = b;
	}
	
	public boolean getCompressionEnabled()
	{
		return this.compressionEnabled;
	}
	
	public Token getRequestToken()
	{
		return this.requestToken;
	}
	
	public void setRequestToken(Token t)
	{
		this.requestToken = t;
	}

	protected HttpClient getHttpClient()
	{
		return this.httpClient;
	}
	
}
